I captured a few thousand(!) packets in 12 seconds(!!) of snooping the entire 900MHz ISM band. These were extracted in code parallel to the ISM Snooping example, with the final bitstream state dumped out in the JSON format at the end. We'll now pull that in and look at the bitstreams.
%pylab inline
pylab.rcParams["figure.figsize"] = 14,4
import scipy
import scipy.signal
import gc
import os
import json
FILENAME = "capture_state_1421785415.json"
f = file(FILENAME)
capture_state = json.load(f)
f.close()
capture_state.keys()
[ capture_state[x] for x in ["filename", "Fc", "Fs"] ]
capture_state['packets'][0].keys()
p = capture_state["packets"][1]
plot(p['bitstream'])
ylim(-0.1, 1.1)
title("Hit %d, baudrate is %d" % (p["hit_i"], p["baudrate"]))
packets = [ p for p in capture_state["packets"] if len(p['bitstream']) > 0 ]
hits = capture_state["merged_hits"]
len(packets)
hist([p['baudrate'] for p in packets ], bins=100); None
hist([np.log10(len(p['bitstream'])) for p in packets], bins=100); None
packets_peakish = [ p for p in packets if len(p['bitstream']) > 100 and len(p['bitstream']) < 150 ]
hist([len(p['bitstream']) for p in packets_peakish], bins=range(100,150)); None
packets_peakish = [ p for p in packets if len(p['bitstream']) > 115 and len(p['bitstream']) < 135 ]
hist([len(p['bitstream']) for p in packets_peakish], bins=range(100,150)); None
hist([(p['baudrate']) for p in packets_peakish], bins=100); None
packets_peakish_slow = [ p for p in packets_peakish if p['baudrate'] < 20000]
hist([len(p['bitstream']) for p in packets_peakish_slow], bins=range(100,150)); None
to_show = []
for p in packets_peakish_slow:
to_add = p['bitstream'] + [0] * (125 - len(p['bitstream']))
to_show.append(to_add)
to_show = np.array(to_show)
to_show.shape
imshow(to_show)
def bitlen_rle(b):
"Returns a RLE of the central binary state lengths, with no concept of parity. Ignores ends."
edges = (b[:-1] != b[1:]).nonzero()[0]
# XXX This neglects the first and last segments. We don't really care, but.
run_lengths = np.diff(edges)
return run_lengths
def find_preamble_end(bitstream):
bp = bitstream[:-1] != bitstream[1:]
bpr = bitlen_rle(bp)
bitpos = 0
for j in bpr:
if j >= 8:
return bitpos+j
bitpos += j
return None
def find_preamble_perfectly(bitstream):
starting_point = find_preamble_end(bitstream)
if None == starting_point:
return None
starting_point -= 5
if starting_point < 0:
starting_point = 0
last_state = bitstream[starting_point]
c = starting_point+1
while c < len(bitstream):
if bitstream[c] == last_state:
return c
last_state = bitstream[c]
c += 1
return c
to_show = []
for p in packets_peakish_slow:
preamble_header = find_preamble_perfectly(np.array(p['bitstream']))
if None == preamble_header:
continue # preamble_header = 0
to_add = p['bitstream'][(preamble_header):] + [0] * (125 - len(p['bitstream']) + preamble_header)
to_show.append(to_add)
to_show.sort()
to_show = np.array(to_show)
to_show.shape
imshow(to_show, cmap=cm.gray_r, interpolation="none")
to_show[100,0:25]
preamble_header = [1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1]
ph = "".join(map(str, preamble_header))
def find_actual_preamble(bitstream):
bs = "".join(map(str, bitstream))
return bs.find(ph)
to_show = []
for p in packets_peakish_slow:
preamble_header = find_actual_preamble(np.array(p['bitstream']))
if -1 == preamble_header:
continue # preamble_header = 0
to_add = p['bitstream'][(preamble_header):] + [0] * (125 - len(p['bitstream']) + preamble_header)
to_show.append(to_add)
to_show.sort()
to_show = np.array(to_show)
to_show.shape
imshow(to_show[:,30:60].T, cmap=cm.gray_r, interpolation="none")
real_data = to_show[:, 32:70]
imshow(real_data.T, interpolation="none", cmap=cm.gray_r)
Pretty. How does the bit distribution look per-line?
plot(np.sum(real_data, axis=1))
So there's a variable number of bits set per-entry at this level. How does each bit do across frames/packets?
real_data = to_show[:, 32:70]
plot(np.sum(real_data, axis=0)/float(real_data.shape[0]))
axhline(0.5, color="red", alpha=0.5)
axvline(3, color="green", alpha=0.7)
axvline(25, color="green", alpha=0.7)
real_data = to_show[:, 32+3:32+25]
Hey now, that looks like our actual packet body. Nice encoding work, people who designed this. Focusing down onto that data range.
plot(np.sum(real_data, axis=0)/float(real_data.shape[0]))
axhline(0.5, color="red", alpha=0.5)
imshow(real_data.T, interpolation="none", cmap=cm.gray_r)
i = 0
while False: # i < real_data.shape[1]:
axhline(i, color="red")
i += 8
plot(np.sum(real_data, axis=1))
Still not even numbers of bits per-packet.
hist(np.sum(real_data, axis=1) % 2); None
And we don't seem to conserve parity in here. Let's see if that's generally true.
bits_set = []
candidate_offsets = range(10, real_data.shape[1])
for i in candidate_offsets:
parities = np.sum(real_data[:,0:i], axis=1) % 2
n = np.sum(parities)
bits_set.append(n)
plot(candidate_offsets, np.array(bits_set)/float(real_data.shape[0]))
ylim(0,1)
Well, doesn't look like any of those are simple parity bits, so this isn't a trivially-checksum'd packet.
What do the deltas look like here?
imshow( (real_data[:-1,:] ^ real_data[1:,:]).T, interpolation="none", cmap=cm.gray_r)
Hrm. Let's reverse this.
real_data_rev = real_data.copy()[:,::-1]
x = real_data_rev.tolist()
x.sort()
real_data_rev = np.array(x)
imshow(real_data_rev.T, interpolation="none", cmap=cm.gray_r)
Sigh. I don't want to brute-force edit distance sort, but, here we go!
distances = np.zeros((real_data.shape[0],real_data.shape[0]))
for i in range(real_data.shape[0]):
for j in range(i+1, real_data.shape[0]):
d = np.sum(real_data[i,:] ^ real_data[j,:])
distances[i,j] = d
distances[j,i] = d
hist(distances.flatten(), bins=range(real_data.shape[0])); None
See that empty 1 box there. That's proof that god hates us.
to_show = []
to_show_h = []
for p in packets_peakish_slow:
preamble_header = find_actual_preamble(np.array(p['bitstream']))
if -1 == preamble_header:
continue # preamble_header = 0
to_add = p['bitstream'][(preamble_header):] + [0] * (125 - len(p['bitstream']) + preamble_header)
to_show.append(to_add)
to_show_h.append( hits[p['hit_i']] )
to_show = np.array(to_show)
real_data = to_show[:, 32+3:32+25]
imshow(real_data.T, interpolation="none", cmap=cm.gray_r)
idx = range(len(to_show_h))
idx.sort(key=lambda i: to_show_h[i]["Fc"])
imshow(real_data[idx,:].T, interpolation="none", cmap=cm.gray_r)
keys = ["Fc", "s_low", "s_length", "bw" ]
for i, k in enumerate(keys):
idx.sort(key = lambda i: to_show_h[i][k])
subplot(2,2,i+1)
imshow(real_data[idx,:].T, interpolation="none", cmap=cm.gray_r)
title("Sorted by " + k)
savefig("ordering_wtf.png", bbox="tight")
Okay, it looks like this is just somebody rolling through codes for some reason. Not sure what it is, but it's what it is.
packets_slow = [ p for p in packets if p['baudrate'] > 9000 and p['baudrate'] < 10000 and len(p['bitstream']) > 150 ]
len(packets_slow)
Whomp whomp. Nothing. On to the faster/bigger stuff.
packets_fast = [ p for p in packets if p['baudrate'] > 50000 and p['baudrate'] < 200000 ]
hist([p['baudrate'] for p in packets_fast ], bins=100); None
packets_fast = [ p for p in packets if p['baudrate'] > 90000 and p['baudrate'] < 110000 and len(p['bitstream']) > 200 ]
hist([p['baudrate'] for p in packets_fast ], bins=100); None
to_show = []
to_show_h = []
l = 200
for p in packets_fast:
to_add = p['bitstream'][:l] + [0] * (l - len(p['bitstream']))
to_show.append(to_add)
to_show_h.append( hits[p['hit_i']] )
to_show = np.array(to_show)
to_show.shape
imshow(to_show, interpolation="none", cmap=cm.gray_r)
keys = ["Fc", "s_low", "s_length", "bw" ]
idx = range(len(to_show))
for i, k in enumerate(keys):
idx.sort(key = lambda i: to_show_h[i][k])
subplot(2,2,i+1)
imshow(to_show[idx,:], interpolation="none", cmap=cm.gray_r)
title("Sorted by " + k)
to_show = []
to_show_h = []
l = 200
for p in packets_fast:
bs = np.array(p['bitstream'])
if len(bs) < 10:
continue
s = find_preamble_perfectly(bs)
if None == s or s < 3 or s > l-10:
continue
to_add_head = list(p['bitstream'][s:s+l])
to_add = to_add_head + [0] * (l - len(to_add_head))
to_show.append(to_add)
to_show_h.append( hits[p['hit_i']] )
to_show = np.array(to_show)
to_show.shape
imshow(to_show.T, interpolation="none", cmap=cm.gray_r)
np.mean([ p['baudrate'] for p in packets_fast])
idx = range(len(to_show))
idx.sort(key = lambda i: to_show_h[i]['Fc'])
imshow(to_show[idx,:].T, interpolation="none", cmap=cm.gray_r)
title("Ordered by frequency")
idx = range(len(to_show))
idx.sort(key = lambda i: to_show_h[i]['s_low'])
imshow(to_show[idx,:], interpolation="none", cmap=cm.gray_r)
title("Ordered by transmission start time")
idx = range(len(to_show))
idx.sort(key = lambda i: to_show_h[i]['s_length'])
imshow(to_show[idx,:], interpolation="none", cmap=cm.gray_r)
title("Ordered by bitstream_length")